/*                                  ATELIER                                   */

/* %%LICENSE_TAG%%                                                            */
package edu.gmu.view.canvas.graph;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.geom.GeneralPath;

import javax.swing.JComponent;
import javax.swing.event.MouseInputListener;

import edu.gmu.view.canvas.AbstractLayer;
import edu.gmu.view.canvas.CursorReadoutAdapter;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.List;

/*******************************************************************
 * Draw a vertical and horizontal line that intersects where the
 * mouse is currently located.
 * 
 * @author  James H. Pope
 * @version $Id:$
 *******************************************************************/
public class GraphLayer extends AbstractLayer implements MouseInputListener, CursorReadoutAdapter
{
    /**
     * 
     */

    public static final DecimalFormat XREADOUT_FORMAT = new DecimalFormat("##0");
    /**
     *
     */
    public static final DecimalFormat YREADOUT_FORMAT = new DecimalFormat("##0.00");
	
    /**
     * Y-Data to draw (dependent variable)
     */
    protected List <Double> yData = null;

    /**
     * X-Data (independent variable) only used for readouts
     */
    protected List <Double> xData = null;

    /**
     * MetaData only used for readouts
     */
    protected List <String> metaData = null;

    /**
     * Vertical Scale
     */
    protected double verticalScale = 1.0;

    /**
     * Horizontal Scale
     */
    protected double horizontalScale = 1.0;

    /**
     * Y-offset
     */
    protected double yOffset = 0.0;

    /**
     * Graphics representation of the data
     */
    protected GeneralPath graph = new GeneralPath();

    /**
     * BackBuffer Image to draw to
     */
    protected BufferedImage bufferedImage = null;

    /**
     * Indication to redraw image
     */
    protected boolean redraw = false;
	
    /**
     * Creates a new GraphLayer with specified parameters.
     */
    public GraphLayer( )
    {
        
    }
    
    /**
     * Gets the graphs verticalScale
     * 
     * @return double verticalScale
     */
    public double getVerticalScale( )
    {
    	return this.verticalScale;
    }
    
    /**
     * Sets the graphs verticalScale
     * 
     * @param verticalScale
     */
    public void setVerticalScale( double verticalScale )
    {
        // Keep between 2^-4 and 2^+4
        if( verticalScale >= 0.0625 && verticalScale <= 16.0 )
        {
            this.verticalScale = verticalScale;
            this.redraw = true;
        }
    }

    /**
     * Gets the graphs horizontalScale
     *
     * @return double horizontalScale
     */
    public double getHorizontalScale( )
    {
    	return this.horizontalScale;
    }

    /**
     * Sets the graphs horizontalScale
     *
     * @param horizontalScale
     */
    public void setHorizontalScale( double horizontalScale )
    {
        // Keep between 2^+0 and 2^+4
        if( horizontalScale >= 1.0 && horizontalScale <= 16.0 )
        {
            this.horizontalScale = horizontalScale;
            this.redraw = true;
        }
    }
    
    /**
     * Gets the yOffset
     * 
     * @return double yOffset
     */
    public double getYoffset( )
    {
    	return this.yOffset;
    }
    
    /**
     * Sets the yOffset
     * 
     * @param yOffset
     */
    public void setYoffset( double yOffset )
    {
    	this.yOffset = yOffset;
        this.redraw = true;
    }
    
    /**
     * Gets the graphs yData
     * 
     * @return List <Double> yData
     */
    public List <Double> getYdata( )
    {
    	return this.yData;
    }
    
    /**
     * Sets the graphs yData
     * 
     * @param yData
     */
//    public void setYdata( List <Double> yData )
//    {
//    	this.yData = yData;
//        this.redraw = true;
//    }
    
    /**
     * Gets the graphs xData
     * 
     * @return List <Double> xData
     */
    public List <Double> getXdata( )
    {
    	return this.xData;
    }
    
    /**
     * Sets the graphs xData
     * 
     * @param xData
     */
//    public void setXdata( List <Double> xData )
//    {
//    	this.xData = xData;
//        this.redraw = true;
//    }
    
    /**
     * Gets the graphs metaData
     * 
     * @return List <String> metaData
     */
    public List <String> getMetadata( )
    {
    	return this.metaData;
    }
    
    /**
     * Sets the graphs metaData
     * 
     * @param metaData
     */
//    public void setMetadata( List <String> metaData )
//    {
//    	this.metaData = metaData;
//        this.redraw = true;
//    }

    /**
     * Sets the graphs data
     *
     * @param yData
     * @param xData
     * @param metaData
     */
    public void setData(  List <Double> yData,
                          List <Double> xData,
                          List <String> metaData )
    {
        this.yData = yData;
        this.xData = xData;
    	this.metaData = metaData;
        this.redraw = true;
    }
    
    
    /*********************************************************************
     * Paints images on specified Component object using passed Graphics.
     *
     * @param component
     * @param graphicsCopy
     *********************************************************************/
    public void paintLayer( final JComponent  component, Graphics2D  graphics )
    {
        int compHeight = component.getHeight();
        int compWidth = component.getWidth();
        if(  this.bufferedImage == null ||
             compHeight != this.bufferedImage.getHeight() ||
             compWidth  != this.bufferedImage.getWidth()    )
        {
            this.bufferedImage = component.getGraphicsConfiguration().createCompatibleImage(compWidth, compHeight, Transparency.BITMASK );
        }

        if( this.redraw == true || this.bufferedImage == null )
        {
            ImageFactory.clearImage(bufferedImage);
            
            Graphics2D imageGraphics = this.bufferedImage.createGraphics();

            this.redraw = false;

            this.draw(component, imageGraphics);
        }
        graphics.drawImage(bufferedImage, 0, 0, null);

        
    }

    /**
     * Draws the data to the component
     */
    public void draw( JComponent component, Graphics2D graphics )
    {
        this.graph.reset(); // clear previous graph
    	if( this.yData != null )
    	{
            /*
             * We are going to modify the graphics object state - which
             * is usually ok when we are just chaning colors, but we are
             * changing the matrices via the affineTransform thus we
             * choose to make a copy that we'll use - then dispose of it
             * when we are done.  Since the copy still points to the same
             * component - the drawing still happens to the same image.
             */
            //Graphics2D graphics = (Graphics2D)graphicsCopy.create();

            graphics.setColor( Color.GREEN );

            // Get the data, scale, draw to GeneralPath, then draw graphics
            for( int dataIndex = 0; dataIndex < this.yData.size(); dataIndex++ )
            {
                /*
                 * Note that lineTo will always round up
                 * so round here instead and then set.
                 * This likely ends up rounding twice
                 * (here and in Path2D - oh well)
                 */
                int yPixelValue = this.convertDataIndexY(dataIndex, component);
                int xPixelValue = this.convertDataIndexX(dataIndex);

                //if( yPixelValue < 0 && yPixelValue > -1000 )
                //{
                //    System.out.println( "Negative value " + yPixelValue + " at " + dataIndex );
                //}
                //xPixelValue = component.getWidth()  - xPixelValue;

                //// Do this for cityscape
                //if( this.yData.size() > 2 )
                //{
                //    double pScaledYvalue = (yData.get(i-1) + this.offset) * this.verticalScale;
                //    long pRoundedYvalue = Math.round( pScaledYvalue );
                //    double pScaledXvalue = (i-1) * this.horizontalScale;
                //    long pRoundedXvalue = Math.round( pScaledXvalue );
                //
                //    graph.lineTo(pRoundedXvalue, pRoundedYvalue);
                //}

                graphics.drawRect(xPixelValue, yPixelValue, 1, 1);
                //if( dataIndex == 0 )
                //{
                //    graph.moveTo(xPixelValue, yPixelValue);
                //}
                //else
                //{
                //    graph.lineTo(xPixelValue, yPixelValue);
                //}

            }

            // Paint the data

            //graphics.draw(graph);


            /*
             * The canvas object (i.e. component) can be used in a ScrollPane
             * if (and only if) we properly set it's preferredSize.  We do
             * so here and also call revalidate.  For multiple layers - this
             * is not the best solution - but works for now.  A better solution
             * would be to add a getPreferredSize to each layer and the
             * canvas could take the union of the results (largest width/height).
             *
             * We only do this when we discover that the data has changed.
             * We keep the scrollPane width twice the size of the data
             * because there seems to be a clipping bug reference the readouts
             * such that when the the
             *
             * data.length == scrollPane.getViewPort.getPreferredWidth
             *
             * the readouts experience quite a bit of clipping - this
             * happens also when it is twice the size - but not as noticable.
             *
             * UPDATE: This clipping bug is a "feature" of the ScrollPane
             * for efficiency reasons, it uses copyImage and assumes the image
             * only needs to be re-created when reavalidate is called.
             */

            /*
             * Gets the bounds from the graph in order to set on the component.
             */
            Rectangle graphBounds = this.graph.getBounds();
            int newWidth  = (int)graphBounds.getWidth() * 2;
            int newHeight = (int)graphBounds.getHeight();

            int previousWidth  = (int)component.getPreferredSize().getWidth();
            int previousHeight = (int)component.getPreferredSize().getHeight();

            if( newWidth != previousWidth || newHeight != previousHeight )
            {
                Dimension preferredSize = new Dimension( newWidth, newHeight );
                component.setPreferredSize(preferredSize);
                /*
                 * Since canvas/component may be a child of JScrollPane
                 * the revalidate forces it to redraw it's children.
                 *
                 * Must call revalidate because the JScrollPane doesn't
                 * realize the preferredSize has changed and won't update
                 * until some other event (e.g. resizing the frame).  This
                 * happens after other EDT tasks (e.g. calling paintLayers).
                 */
                //component.revalidate();
            }
            graphics.dispose();
    	}
    }
    

    //------------------------------- Readouts ------------------------------//
    /**
     * Converts given x and y into readout.
     *
     * @param xPixelIndex
     * @param yPixelIndex 
     * @param component
     * @return String converted value
     */
    public String convertPosition( int xPixelIndex, int yPixelIndex, JComponent  component )
    {
        int dataX = this.convertXPixelIndex(xPixelIndex);
    	String readoutX = null;
    	if( this.xData != null && dataX < this.xData.size() )
    	{
    		double dataValue = this.xData.get(dataX);
    		readoutX = XREADOUT_FORMAT.format(dataValue);
    	}
    	else
    	{
    		return null;
    	}
    	
    	String readoutY = null;
    	if( this.yData != null && dataX < this.yData.size() )
    	{
    		double dataValue = this.yData.get(dataX);
    		readoutY = YREADOUT_FORMAT.format(dataValue);
    	}
    	else
    	{
    		return null;
    	}
    	
    	String readoutMetaData = "";
    	if( this.metaData != null && dataX < this.metaData.size() )
    	{
    		readoutMetaData = this.metaData.get(dataX);
    	}

        String readout = readoutX +", " + readoutY + "  " + readoutMetaData;
        //readout = readout + " x,y=" + xPixelIndex + "," + yPixelIndex;
    	return readout;
    }
    
    /**
     * Converts the dataIndex index into a xPixelIndex.
     * 
     * @param dataIndex
     * @return xPixelIndex
     */
    public int convertDataIndexX( int dataIndex )
    {
        double scaledXvalue = dataIndex * this.horizontalScale;
        int roundedXvalue = Math.round( (float)scaledXvalue );
        return roundedXvalue;
    }

    /**
     * Converts the dataIndex index into a yPixelIndex.
     *
     * @param dataIndex
     * @param component
     * @return yPixelIndex
     */
    public int convertDataIndexY( int dataIndex, JComponent component )
    {
        double scaledYvalue = (yData.get(dataIndex) + this.yOffset) * this.verticalScale;
	    int roundedYvalue = Math.round( (float)scaledYvalue );
        /*
         * Note we translate/flip without using transforms.
         * In other words, we adjust the data prior to drawing.
         * This is currently preferable as it is easier to understand
         * and keep all layers in sync with their drawing.
         */
        roundedYvalue = component.getHeight() - roundedYvalue;
        return roundedYvalue;
    }

    /**
     * Converts the xPixelIndex into a dataIndex.
     *
     * @param xPixelIndex
     * @return dataX index (may or may not be valid dataIndex)
     */
    public int convertXPixelIndex( int xPixelIndex )
    {
        int dataIndex = (int)Math.round(xPixelIndex  / this.horizontalScale);
        return dataIndex;
    }
    
    //------------------------- Delegated MouseEvents -----------------------//
    public void mouseClicked(MouseEvent e)
    {
        
    }

    public void mousePressed(MouseEvent e)
    {
        
    }

    public void mouseReleased(MouseEvent e)
    {
        
    }

    public void mouseEntered(MouseEvent e)
    {
        
    }

    public void mouseExited(MouseEvent e)
    {
        
    }

    public void mouseDragged(MouseEvent e)
    {
        
    }

    public void mouseMoved(MouseEvent e)
    {
        
    }
     
}

/*                                  ATELIER                                   */
